package com.he.fetch.util;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Set;

import com.he.fetch.model.FetchField;
import com.he.fetch.model.FetchSet;

/**
 * <code>FetchHelper</code> provides support for invoking "getters" along a set
 * of supplied paths. It is intended to implement controlled fetching in JPA
 * environment.
 * 
 * @author Abhai Raj Singh
 * 
 */
public class FetchHelper {

    public static final String FETCHSET = FetchSet.class.getSimpleName();

    private String constructMethodName(Class<?> clazz, String fieldName) {
	String name = "get" + fieldName.substring(0, 1).toUpperCase()
		+ fieldName.substring(1);
	try {
	    clazz.getMethod(name);
	} catch (Exception e) {
	    name = "is" + fieldName.substring(0, 1).toUpperCase()
		    + fieldName.substring(1);
	}
	return name;
    }

    private void fetch(Object object, Set<FetchField> fetchFields)
	    throws Exception {
	if (object == null) {
	    return;
	} else {
	    for (FetchField fetchField : fetchFields) {
		if (object instanceof Collection<?>) {
		    Collection<?> collection = (Collection<?>) object;
		    for (Object member : collection) {
			fetch(member, fetchFields);
		    }
		} else {
		    String methodName = constructMethodName(object.getClass(),
			    fetchField.getName());
		    Method method = object.getClass().getMethod(methodName);
		    fetch(method.invoke(object), fetchField.getSuccessors());
		}
	    }
	}
    }

    /**
     * 
     */
    public void fetch(Object object, String fetchSetName) throws Exception {
	FetchSet fetchSet = Factory.getInstance()
		.getFetchFieldSet(fetchSetName);
	fetch(object, fetchSet.getSet());
    }

    /**
     * Uses reflection to traverse paths constructed from fetches array
     * elements. For example, if <code>object</code> were a <code>User</code>
     * object and <code>fetches</code> were
     * <code>{"accounts.name", "phoneNumber.value"}</code>, this method would
     * call <code>getAccounts()</code> on <code>object</code>, iterate through
     * the collection of accounts and call <code>getName()</code> on each
     * account. Likewise it would call <code>getPhoneNumber()</code> on
     * <code>object</code> to return a <code>PhoneNumber</code> object and then
     * call <code>getValue()</code> on that object.
     * 
     * The code relies on POJO coding conventions and assumes the method name
     * prefix to try first is <code>get</code>. If <code>get</code> doesn't
     * work, it tries <code>is</code>.
     * 
     * @param object
     *            - The POJO that is root of a graph
     * @param fetches
     *            - An array of paths to traverse. Each element of the array is
     *            one path. A path is a dot(".")-separated string of field
     *            names. For example,
     *            <code>{"accounts.name", "phoneNumber.value"}</code>
     * @throws Exception
     *             - {@link NoSuchMethodException}, {@link SecurityException},
     *             {@link IllegalAccessException},
     *             {@link InvocationTargetException},
     *             {@link IllegalArgumentException}
     */
    public void fetch(Object object, String[] fetches) throws Exception {
	if (object == null || fetches == null || fetches.length == 0) {
	    return;
	}
	if (fetches[0].equals(FETCHSET)) {
	    fetch(object, fetches[1]);
	} else {
	    for (int i = 0; i < fetches.length; i++) {
		String fetch = fetches[i];
		fetch(object, fetch.split("\\."), 0);
	    }
	}
    }

    private void fetch(Object object, String[] path, int position)
	    throws Exception {
	if (object == null) {
	    return;
	} else {
	    if (position < path.length) {
		if (object instanceof Collection<?>) {
		    Collection<?> collection = (Collection<?>) object;
		    for (Object member : collection) {
			fetch(member, path, position);
		    }
		} else {
		    String methodName = constructMethodName(object.getClass(),
			    path[position]);
		    Method method = object.getClass().getMethod(methodName);
		    fetch(method.invoke(object), path, position + 1);
		}
	    }
	}
    }

}
